Repository navigation
fix(expression): report a character outside the grammar without closing the connection - #567
Open
LeeroyHannigan wants to merge 5 commits into
Open
LeeroyHannigan wants to merge 5 commits into
LeeroyHannigan wants to merge 5 commits into
Conversation
…e does, without a panic The tokenizer behind condition and filter expressions read the offending byte as a char and sliced the input five bytes either side of it. When that range ended inside a multi-byte character (`#v = ééé`, `#v = 日本語`) the slice panicked, the request task ended and the connection closed; the server's key condition parse ahead of authorization calls the same tokenizer, so a Query key condition did the same for every principal. Other multi-byte input answered a token made of the first byte read as Latin-1. The tokenizer behind update, projection and the engine's key condition parse did not panic and answered the character with one byte after it as the window. The error now names the character as sent and shows the service's window: the source text from the start of the previous token to the end of the next one, with each character outside the grammar counted as one token (measured 2026-10-25, each request twice: `#v = ééé` answers `token: "é", near: "= éé"`, `abéc = :v` answers `near: "abéc"`, `#v = $x` answers `near: "= $x"`). Both tokenizer entry points share the rule.
Pure move, using the #[path] pattern evaluator_tests.rs uses; tokenizer.rs had grown past 600 lines with the previous commit.
…rammar Every row is a request measured against the service on 2026-10-25, each sent twice: Scan filters with multi-byte and ASCII characters outside the grammar in each position, and one or two rows each for PutItem conditions, Query key conditions, UpdateItem update expressions and GetItem projections.
…drop U+0000, as the service does When no token comes before the character, the service starts the window at the start of the expression and keeps the leading whitespace (` é = :v` answers `near: " é ="`), and it removes U+0000 from the token and the window (`#v = \0x` answers `token: "", near: "= x"`). Measured 2026-10-25 at 16:34, 16:38 and 16:43 UTC, each request twice. The unit table gains those rows and the rows that pin the next-token rules for two-character operators, name runs and digit runs.
…ken rules and the Query paths Rows measured on 2026-10-25 at 16:34, 16:38, 16:43 and 16:54 UTC, each request twice: a character with no token before it, U+0000, two-character operators, name runs and digit runs after the character, and the panic shape on a Query key condition, a Query filter and an UpdateItem condition. The prefix the service adds on write paths is stripped only on those paths.
LeeroyHannigan
requested review from
amrith,
c33howard,
jcshepherd,
pdf-amzn,
robinnsc and
yesyayen
as code owners
October 25, 2026 17:34
This branch has not been deployed
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
A character outside the expression grammar, such as
é, could close the connection instead of answering a syntax error. It now answers the syntax error DynamoDB gives, with the same token and window.What happened on main
The tokenizer behind condition and filter expressions read the offending byte as a char and sliced the input five bytes either side of it. When that range ended inside a multi-byte character (
#v = ééé,#v = 日本語), the slice panicked, the request task ended and the client saw the connection close. This reached:ConditionExpressionon PutItem, UpdateItem, DeleteItem and TransactWriteItems;FilterExpressionon Scan and Query;KeyConditionExpressionon Query, through the server'sdynamodb:LeadingKeysparse, which runs before the authorization decision for every caller.Other multi-byte input on those paths answered a token made of the first byte read as Latin-1 (
token: "Ã") with a byte window. The tokenizer behindUpdateExpression,ProjectionExpressionand the engine's key condition parse did not panic; it answered the character with one byte after it as the window (near: "é",near: "$x").What the service answers
Measured on 2026-10-25 at 16:09, 16:34, 16:38, 16:43 and 16:54 UTC in us-east-1, each request sent twice with the same answer, on all five expression kinds. When the character is the first fault in the expression:
é,日,😀, or$,@and!for ASCII characters outside the grammar.#v = éééanswerstoken: "é", near: "= éé";abéc = :vanswersnear: "abéc";#v = é abcanswersnear: "= é abc";#v é<> :vanswersnear: "#v é<>";contains(#v, é)answersnear: ", é)".é = :vanswersnear: " é =".#v = \u0000xanswerstoken: "", near: "= x".Both tokenizer entry points now follow this rule.
Tests
crates/core/src/expression/tokenizer_tests.rs: one table test with 44 measured rows, run through both entry points. Each of these altered versions of the change fails it: the window starting at the character, the window not reaching the next token, the token read as a byte, no two-character operators, no name runs, no digit runs, and U+0000 kept.tests/test_expression_unknown_character.py: 50 dual-target wire tests on Scan and Query filters, PutItem and UpdateItem conditions, Query key conditions, UpdateItem update expressions and GetItem projections. The Query key condition rows include the shape that closed the connection on main.Run locally on 2026-10-25 against a SQLite release build:
cargo +1.97.0 fmt --all -- --check,cargo +1.97.0 clippy -p extenddb-core --all-targets -- -D warnings, andcargo test -p extenddb-coreon stable (521 passed) and on 1.88.0 (520 passed).The full SQLite wire suite at the third commit had 2 failures, in
tests/test_gsi_async.py, which starts a server from./target/release/extenddb, a path that did not exist in that run; main fails the same 2. CI runs the new file inrun-integration,run-integration-sqlite,mongodb-pytestandcassandra-pytest.Not changed here
These differences from the service are on main and stay as they are:
#v = = é), the service reports that fault and ExtendDB reports the character, because ExtendDB tokenizes the whole expression before parsing.#or:followed by a character that cannot start a name keeps main's texts (empty name reference '#',empty value placeholder ':'). The service answerstoken: "#", near: "#é"by the same window rule.#v = 1: the service refuses a bare number as an operand; ExtendDB reads it as an attribute name.; expression size: <n>on filter and condition expressions; ExtendDB does.1 validation error detected:prefix the service adds on write paths.Not measured: the service's answer to a caller fenced by
dynamodb:LeadingKeysfor such a key condition. ExtendDB answers ValidationException underForAllValuesand AccessDeniedException underForAnyValue, with no items either way.Files
tokenizer.rspassed 600 lines with the fix, so the second commit moves its tests intotokenizer_tests.rswith no other change, using the#[path]pattern ofevaluator_tests.rs.tokenizer.rsis now 449 lines.Merging
git merge-tree --write-treeis clean against #558, #511 and #479. #558 builds a window for parser faults from the same rule. Its window also starts at the first token when nothing comes before the fault, where the service starts at the start of the expression; that is fixed on #558 separately.